package lab_2_server;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import lab_2_server.handlers.ClientHandler;
import lab_2_server.handlers.AbstractHandler;
import lab_2_server.handlers.ServerHandler;
import org.jdom2.*;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

/**
 * Main class in project
 * @author pacific
 */
public class Server extends Thread{

    /** Constructor
     * @param newMonitor reference to UserInterface
     */
    public Server(ServerMonitorUI newMonitor) {
        setMonitor(newMonitor);
    }

    public Server( ServerMonitorUI newMonitor, int port ) {
        setMonitor( newMonitor ) ;
        setPort( port ) ;
    }
    

    @Override
    public void run() {
        // Load saved cofig if exist
        loadConfig() ;
        //Connect to all servers in list
        initServersHandlers() ;
        //Create and run pull of handlers
        initClientsHandlers() ;
        workServer() ;
    }
    
    //Method callable before exit
    public void exit() {
        for( ServerHandler currHandler : getServerHandlers() ) {
            currHandler.disconnect() ;
        }

        saveConfig() ;
    }


    /**
     * Main method of currentServer
     */
    private void workServer() {
        //создаём сокет для сервера
        ServerSocket serverSocket = initServer() ;

        //If not run currentServer socket, to exit!
        if( serverSocket == null )
            System.exit(1) ;

        //Main cycle
        while (!Thread.interrupted()) {
            Socket socket = null;
            try {
                MyLogger.putMessange("Wait connect with client.") ;
                socket = serverSocket.accept();
                MyLogger.putMessange("Connect with client has been.");
            } catch (IOException ex) {
                MyLogger.putMessange("Not can listen port i|o exception ERROR");
                System.exit(1);
            }

            //Exclude handler from pull
            MyLogger.putMessange("Wait free  client handler.") ;
            //Sleep if not are free handler
            final int TIME_TO_SLEEP = 200 ;
            while ( getHandlersPull().isEmpty() ) {
                MyLogger.putMessange(getHandlersPull()+"") ;
                synchronized (this) {
                    try {
                        wait( TIME_TO_SLEEP );
                    } catch ( InterruptedException ex ) {
                        MyLogger.putMessange(ex);
                    }
                }
            }
            MyLogger.putMessange("Free client handler is it.") ;

            //Move handler from "Pull handler" to "Client handler"
            ClientHandler handler = getHandlersPull().get(0);
            getHandlersPull().remove(handler) ;
            getClientsHandlers().add(handler) ;

            MyLogger.putMessange("Notify client handler.") ;
            handler.setSocket(socket) ;
            synchronized(handler) {
                handler.notify() ;
            }
        }
        
    }


    /**Connect to all servers in list*/
    public synchronized void initServersHandlers() {
        MyLogger.putMessange( "InitServerHandlers" ) ;
        for( ServerID serverID : getServersID() ) {
            //if already connected, skip iteration
            if( serverID.isConnected() ) {
                continue ;
            }
            //Create and run new currentServer handler
            //If he connected, he add self to ServerHandlers
            ServerHandler handler = new ServerHandler(this, serverID) ;
            Thread thread = new Thread( handler ) ;
            thread.start() ;
        }
    }


    /** Initiate handlers of clients (Create storage) */
    private void initClientsHandlers() {
        MyLogger.putMessange("InitClientsHAndlers") ;
        //Create pull of handlers
        for (int i = 0; i < getHandlersMax(); i++) {
            getHandlersPull().add(new ClientHandler(this));
        }
        //Start all thread in pull
        for( ClientHandler handler : getHandlersPull() ) {
            Thread thread = new Thread(handler) ;
            thread.setDaemon(true) ;
            thread.start() ;
        }
    }


    /**
     * Try run currentServer on selected port
     * @return getted ServerSocket
     */
    private ServerSocket initServer() {
        MyLogger.putMessange("InitServer") ;
        ServerSocket serverSocket = null ;

        //Try run ServerSocket
        int tryCount = 30 ;
        for ( int currentTry = 0 ;
                serverSocket == null && !Thread.interrupted() &&
                currentTry < tryCount ;
                currentTry++ )
        {
            try {
                serverSocket = new ServerSocket( getPort() );
                MyLogger.putMessange("Server is runned on port: " + getPort() );
            }
            catch (IOException ex) {
                MyLogger.putMessange( "Not can run server on port " + getPort() + ". Try number " + (currentTry + 1));
                try {
                    synchronized (this) {
                        wait(300);
                    }
                } catch (InterruptedException ex1) {
                    MyLogger.putMessange("Get exception in try run server.");
                }
            }
        }
        
        return serverSocket ;
    }
    

    //Constants for save in XML, uses in "loadConfig(), saveConfig()"
    protected static final String CONF_ROOT = "Server_config" ;
    protected static final String CONF_MY_PORT = "Port_of_this_server" ;
    protected static final String CONF_NUMBER_HANDLERS = "Number_of_handlers" ;
    protected static final String CONF_OTHER_SERVERS = "Other_servers" ;
    protected static final String CONF_SAMPLE_SERVER = "Other_server" ;
    protected static final String CONF_OTHER_HOST = "Host" ;
    protected static final String CONF_OTHER_PORT = "Port" ;
    protected static final String CONF_OTHER_ALIAS = "Alias" ;
    
    /**
     * Method save config from memory in file,
     * call this method before exit
     */
    private void saveConfig() {
        Element root = new Element(CONF_ROOT) ;
        Document document = new Document(root) ;
        
        //  save port for working this currentServer
        Element thisPort = new Element(CONF_MY_PORT) ;
        thisPort.setText(""+getPort()) ;
        root.addContent(thisPort) ;

        // save number of handlers
        Element numbersOfHandlers = new Element(CONF_NUMBER_HANDLERS) ;
        numbersOfHandlers.setText(""+getHandlersMax()) ;
        root.addContent(numbersOfHandlers) ;
        
        //-------Save other servers-------

        Element otherServers = new Element(CONF_OTHER_SERVERS) ;
        Element exampleServer, otherHost, otherPort, alias ;

        //save currentServer for example (not loaded)
        otherHost = new Element(CONF_OTHER_HOST) ;
        otherHost.setText("0.0.0.0") ;
        otherPort = new Element(CONF_OTHER_PORT) ;
        otherPort.setText(""+0000) ;
        alias = new Element(CONF_OTHER_ALIAS) ;
        alias.setText("Server for example (change my)");
        exampleServer = new Element(CONF_SAMPLE_SERVER);
        exampleServer.addContent(otherHost);
        exampleServer.addContent(otherPort);
        exampleServer.addContent(alias);
        otherServers.addContent(exampleServer);

        //save other servers
        for( ServerID currentServer : getServersID() ) {
            alias = new Element(CONF_OTHER_ALIAS) ;
            alias.setText(currentServer.getAlias()) ;
            otherHost = new Element(CONF_OTHER_HOST) ;
            otherHost.setText(currentServer.getHost()) ;
            otherPort = new Element(CONF_OTHER_PORT) ;
            otherPort.setText(""+currentServer.getPort()) ;
            
            exampleServer = new Element(CONF_SAMPLE_SERVER) ;
            exampleServer.addContent(otherHost) ;
            exampleServer.addContent(otherPort) ;
            exampleServer.addContent(alias) ;
            otherServers.addContent(exampleServer) ;
        }
        
        root.addContent( otherServers ) ;

        //write servers in file
        XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
        FileWriter writer = null;
        try {
            writer = new FileWriter(getConfigFile());
            outputter.output(document, writer);
            writer.flush();
        } catch (IOException ex) {
            MyLogger.putMessange("Error on write configs in file");
        } finally {
            try {
                writer.close();
            } catch (IOException ex) {
                MyLogger.putMessange("Error on close writer for save configs");
            }
        }
    }


    /**
     * Method load configs from file and
     * save his in memory
     */
    private void loadConfig() {
        if (getConfigFile().canRead()) {
            SAXBuilder builder = new SAXBuilder();
            Document document = null ;
            try {
                document = builder.build(getConfigFile());
            } catch (Exception ex) {
                MyLogger.putMessange( "Error on read from file. " + ex ) ;
            }
            Element root = document.getRootElement();

            //set config for this currentServer
            int portNumber = Integer.parseInt(root.getChildText(CONF_MY_PORT)) ;
            setPort( portNumber ) ;

            String numberHandlersStr = root.getChildText(CONF_NUMBER_HANDLERS);
            setHandlersMax(Integer.parseInt(numberHandlersStr));

            //add other servers in list
            Element otherServers = root.getChild(CONF_OTHER_SERVERS);
            List<Element> serversList = otherServers.getChildren(CONF_SAMPLE_SERVER);
            HashSet<ServerID> newServerSet = new HashSet<ServerID>();
            Iterator<Element> iterServerList = serversList.iterator();
            String hostOtherServer, aliasOtherServer;
            int portOtherServer;
            while (iterServerList.hasNext()) {
                Element element = iterServerList.next();
                hostOtherServer = element.getChildText(CONF_OTHER_HOST);
                portOtherServer = Integer.parseInt(element.getChildText(CONF_OTHER_PORT));
                aliasOtherServer = element.getChildText(CONF_OTHER_ALIAS);

                // Skip option for example
                if (hostOtherServer.equals("0.0.0.0") && portOtherServer == 0) {
                    continue;
                }

                newServerSet.add(new ServerID(hostOtherServer, portOtherServer, aliasOtherServer));
            }
            getServersID().addAll(newServerSet);
        }
    }


    //-----Fields-----

    //Reference to GUI of currentServer
    private ServerMonitorUI monitor ;
    //Port for ServerSocket (By default 6666)
    private int port ;
    //File for configurations
    private File configFile = new File( "ConfigServer.xml" ) ;
    //List of Servers ID
    private HashSet<ServerID> servers = new HashSet<ServerID>() ;
    //Storage for runned client handler
    private Vector<ClientHandler> clientsHandlers = new Vector<ClientHandler>() ;
    //Storage for runned currentServer handlers
    private Vector<ServerHandler> serversHandlers = new Vector<ServerHandler>() ;
    //Pull of handlers
    private Vector<ClientHandler> handlersPull = new Vector<ClientHandler>() ;
    //Map of clients
    private HashMap<Integer,AbstractHandler> idToHandler = new HashMap<Integer,AbstractHandler>() ;
    //counter for allocation id for client
    private int counterID = 0 ;
    public synchronized int getNewId() {
        return ++counterID ;
    }
    //Max number of user handlers
    private int handlersMax = 10 ;


    //------GETTERS and SETTERS------

    //for "port"
    private int getPort() {
        return port;
    }
    private void setPort( int newPort ) {
        this.port = newPort ;
    }
    //For "Server's IDs"
    public HashSet<ServerID> getServersID() {
        return servers;
    }
    //For client's handlers
    public Vector<ClientHandler> getClientsHandlers() {
        return clientsHandlers;
    }
    //For number of max handlers
    private int getHandlersMax() {
        return handlersMax;
    }
    private void setHandlersMax(int newhandlersMax) {
        handlersMax = newhandlersMax;
    }
    //For "Config File"
    private File getConfigFile() {
        return configFile;
    }
    //For "GUI monitor"
    public ServerMonitorUI getMonitor() {
        return monitor;
    }
    private void setMonitor(ServerMonitorUI monitor) {
        this.monitor = monitor;
    }
    //For "Server's Handlers"
    public Vector<ServerHandler> getServerHandlers() {
        return serversHandlers ;
    }
    //For "Pull of Client's Handlers"
    public Vector<ClientHandler> getHandlersPull() {
        return handlersPull;
    }
    //For "Map 'ID of User' TO 'Handler'"
    public HashMap<Integer, AbstractHandler> getMapIdToHandler() {
        return idToHandler;
    }
    
}
